package com.practice.chatroom;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.Base64;
import java.util.Scanner;
import java.util.stream.Stream;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.ScrollPane.ScrollBarPolicy;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.layout.BorderWidths;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.stage.Stage;

/**
 * 本类为配套gui客户端，可与浏览器分别登录不同用户，进行群聊 与浏览器相比，暂不支持：注册、传图片（可以接）、查看历史聊天记录、自定义字色字号
 */
public class ChatRoomClient extends Application {

    /** 整体界面宽度 */
    private static final int HEIGHT = 1000;
    /** 整体界面高度 */
    private static final int WIDTH = 800;
    /** 默认间距 */
    private static final int DEFAULT_PADDING = 8;
    /** 组件默认内边距 */
    private static final Insets DEFAULT_INSETS = new Insets(DEFAULT_PADDING);
    /** 组件默认圆角边框 */
    private static final CornerRadii DEFAULT_RADII = new CornerRadii(DEFAULT_PADDING);
    /** 组件默认样式边框 */
    private static final Border DEFAULT_BORDER = new Border(
            new BorderStroke(Color.GREY, BorderStrokeStyle.SOLID, DEFAULT_RADII, new BorderWidths(2)));
    /** 默认字体 */
    private static final Font DEFAULT_FONT = Font.font("宋体", 16); // 默认字体

    /** 登录后的聊天主界面 */
    private Stage stageMain = new Stage(); // 登录后的聊天主界面
    /** 登录界面 */
    private Stage stageLogin;
    /** 根布局的左区域根容器 */
    private VBox left = new VBox();
    /** 根布局的中心区域主容器 */
    private VBox center = new VBox();
    /** 左区域根容器内的首层主容器 */
    private VBox leftBox = new VBox();
    /** 程序图标 */
    private Image icon;
    /** 聊天消息区主容器 */
    private VBox msgArea = new VBox();
    /** 聊天消息区主容器外层的滚动条载体 */
    private ScrollPane sp = new ScrollPane();

    private Socket socket;
    private OutputStream socketOS;
    /** 用户名全局变量 */
    private String localUserName;

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage stage) throws Exception { // 加载程序
        stageLogin = stage;
        icon = new Image(new FileInputStream(new File("src/main/resources/imgs/D.png")));
        initLogin(); // 初始化登录界面组件

        initMain(); // 初始化聊天主界面组件

        stageMain.setOnCloseRequest(event -> { // 退出程序时关闭socket
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        });

        stage.show(); // 展示初始界面
    }

    /**
     * 设置登录按钮事件
     * 
     * @param btnLogin 按钮组件
     * @param host     连接的服务端ip
     * @param portText 服务器端口
     * @param user     用户名
     * @param pw       密码
     */
    private void loginBtn(Button btnLogin, TextField host, TextField portText, TextField user, PasswordField pw) {
        btnLogin.setOnAction(event -> { // 登录
            try {
                String hostIP = host.getText();
                String port = portText.getText();
                if (hostIP.trim().isEmpty() || port.trim().isEmpty()) {
                    (new Alert(AlertType.ERROR, "地址或端口不能为空！", ButtonType.CLOSE)).show();
                    return;
                }
                socket = new Socket(hostIP, Integer.parseInt(port)); // 发起交互
                Scanner socketSc = new Scanner(socket.getInputStream(), "UTF-8");
                socketOS = socket.getOutputStream();
                localUserName = user.getText();
                String data = "CLIENT\r\nusername=" + localUserName + "&password=" + pw.getText() + "\r\n\r\n"; // 为方便与http请求一致初始处理，也以完整空行为结束
                socketOS.write(data.getBytes("UTF-8"));
                socketOS.flush();
                keepReceive(socket, socketSc);
            } catch (NumberFormatException | IOException e) {
                e.printStackTrace();
                (new Alert(AlertType.ERROR, e.getMessage(), ButtonType.CLOSE)).show(); // 弹出报错提示
            }
        });
    }

    /**
     * 建立连接，子线程阻塞式接受交互
     * 
     * @param socket   已连接的socket
     * @param socketSc 输入流套的scanner
     */
    private void keepReceive(Socket socket, Scanner socketSc) {
        new Thread(() -> { // 必须在独立的线程上交互，不能阻塞主线程
            try {
                while (socketSc.hasNextLine()) { // 约定每次发2行，首行为通信类型，第二行为数据
                    String line = socketSc.nextLine();
                    System.out.println(line);
                    JSONObject data = new JSONObject(line); // 为方便兼容浏览器，统一用json串交互
                    Platform.runLater(() -> { // jfx不允许非主线程更改ui，因此必须放入这个特殊方法，感觉类似yield让渡回主线程
                        switch (data.getString("msgType")) {
                        case "logined": // 登录成功
                            stageMain.show();
                            stageLogin.hide(); // 切换界面
                            break;
                        case "refreshOnlineUser": // 刷新在线用户
                            refreshOnlineUser(data.getJSONArray("datas"));
                            break;
                        case "spreadMsg": // 有新消息
                            receiveMsg(data.getJSONObject("datas"));
                            break;
                        default:
                            (new Alert(AlertType.ERROR, data.getString("errMsg"), ButtonType.CLOSE)).show(); // 服务端抛错
                        }
                    });
                }
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }).start();
    }

    /**
     * 刷新在线用户
     * 
     * @param json 用户数据json数组
     */
    private void refreshOnlineUser(JSONArray json) {
        ObservableList<Node> users = leftBox.getChildren();
        while (users.size() > 1) { // 移除先前的在线列表
            users.remove(users.size() - 1);
        }
        json.forEach(user -> { // 每个用户追加一个label
            users.add(getMargin(DEFAULT_PADDING));
            JSONObject userData = (JSONObject) user;
            String username = userData.getString("username");
            Label userLabel = new Label(username, buildImgByBase64(userData.getString("icon"), 32, 32)); // 带图label，文字为用户名，图标为头像
            userLabel.setPrefWidth(leftBox.getPrefWidth() - DEFAULT_PADDING * 2);
            userLabel.setPadding(DEFAULT_INSETS);
            userLabel.setFont(DEFAULT_FONT);
            if (username.equals(localUserName)) { // 亮色标出自己
                userLabel.setBackground(getColorBg(Color.PALEGREEN));
            }
            userLabel.setBorder(getRadiiBorder(Color.PALEGREEN));
            leftBox.getChildren().add(userLabel);
        });
    }

    /**
     * 接收新消息
     * 
     * @param datas 消息json
     */
    private void receiveMsg(JSONObject datas) {
        try { // 每条消息追加一组由box层层包裹的结构
            VBox msgRow = new VBox();
            VBox msgBox = new VBox();
            msgBox.setMaxWidth((WIDTH - left.getPrefWidth()) * 0.7);
            String username = datas.getString("username");
            String time = datas.getString("timestamp").substring(11, 19);
            Label msgTitle = new Label();
            msgTitle.setFont(DEFAULT_FONT);

            String str = URLDecoder.decode(datas.getString("msg"), "UTF-8"); // 为支持中文、图片、特殊字符且与浏览器端兼容，交互中都要进行编解码
            ImageView img = null;
            if (datas.getBoolean("isImg")) { // 图片消息
                img = buildImgByBase64(str, 0, 0);
                str = null;
            }
            Label msg = new Label(str, img);
            msg.setPadding(DEFAULT_INSETS);
            msg.setBorder(getRadiiBorder(null));
            msg.setFont(DEFAULT_FONT);
            msg.setWrapText(true);

            if (username.equals(localUserName)) { // 自己发出的消息样式
                msgRow.setAlignment(Pos.CENTER_RIGHT);
                msgBox.setAlignment(Pos.CENTER_RIGHT);
                msgTitle.setText(time + " " + username);
                msgTitle.setTextFill(Color.GREEN);
                msg.setBackground(getColorBg(Color.PALEGREEN));
            } else { // 别人的消息样式
                msgTitle.setText(username + " " + time);
                msgTitle.setTextFill(Color.BLUE);
                msg.setBackground(getColorBg(Color.WHITE));
            }

            msgBox.getChildren().add(msgTitle);
            msgBox.getChildren().add(getMargin(DEFAULT_PADDING));
            msgBox.getChildren().add(msg);
            msgBox.getChildren().add(getMargin(DEFAULT_PADDING * 2));

            msgRow.getChildren().add(msgBox);
            msgArea.getChildren().add(msgRow);

            Platform.runLater(() -> sp.setVvalue(Double.MAX_VALUE)); // 滚动到底部目前效果欠佳
        } catch (UnsupportedEncodingException | JSONException e) {
            e.printStackTrace();
        }
    }

    /**
     * 由base64串生成一个图像
     * 
     * @param base64 同html img的src的串
     * @param width  生成图像的宽度，可以不传
     * @param height 生成图像的高度，可以不传
     * @return 承载该base64串生成图像的ImageView组件
     */
    private ImageView buildImgByBase64(String base64, double width, double height) {
        base64 = base64.substring(base64.indexOf(";base64,") + 8); // 与网页src不同的是，图片格式等固定前缀要去掉
        ByteArrayInputStream bytes = new ByteArrayInputStream(Base64.getDecoder().decode(base64));
        if (width > 0 && height > 0) {
            return new ImageView(new Image(bytes, width, height, true, true));
        } else {
            return new ImageView(new Image(bytes));
        }
    }

    /**
     * 设置发消息按钮
     * 
     * @param btn   按钮组件
     * @param input 输入消息的文本组件
     */
    private void sendBtn(Button btn, TextArea input) {
        btn.setOnAction(event -> {
            try {
                if (input.getText().length() < 1) {
                    return;
                }
                socketOS.write((URLEncoder.encode(input.getText(), "UTF-8") + "\r\n").getBytes("UTF-8"));
                socketOS.flush();
                input.setText("");
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
    }

    /**
     * 初始化登录界面的组件
     * 
     * @throws Exception
     */
    private void initLogin() throws Exception {
        HBox loginRoot = new HBox();
        stageLogin.setScene(new Scene(loginRoot, HEIGHT, WIDTH));
        stageLogin.setTitle("登录DWY的聊天室");
        stageLogin.getIcons().add(icon);

        loginRoot.setBackground(new Background(new BackgroundFill(Color.PLUM, null, null)));
        loginRoot.setAlignment(Pos.CENTER);
        VBox loginBox = new VBox();
        loginRoot.getChildren().add(loginBox);

        loginBox.setAlignment(Pos.CENTER_LEFT);
        loginBox.setPrefWidth(WIDTH / 3);

        Label hostLabel = new Label("服务器");
        loginBox.getChildren().add(hostLabel);
        HBox hostBox = new HBox();
        loginBox.getChildren().add(hostBox);
        hostBox.setAlignment(Pos.BASELINE_LEFT);
        TextField host = new TextField("192.168.0.101");
        TextField portText = new TextField("9000");
        portText.setPrefWidth(120);
        hostBox.getChildren().addAll(host, portText);

        Label usernameLabel = new Label("用户名");
        TextField user = new TextField();
        Label passwordLabel = new Label("密码");
        PasswordField pw = new PasswordField();
        Platform.runLater(() -> user.requestFocus());

        loginBox.getChildren().addAll(usernameLabel, user, passwordLabel, pw);

        loginBox.getChildren().add(getMargin(DEFAULT_PADDING));
        Button btnLogin = getDefaultSizeBtn("登录");
        btnLogin.setFont(DEFAULT_FONT);
        HBox btnBox = new HBox(btnLogin);
        btnBox.setAlignment(Pos.CENTER_RIGHT);
        loginBox.getChildren().add(btnBox);

        // 统一设置字体、内边距
        Stream.of(new Label[] { hostLabel, usernameLabel, passwordLabel }).forEach(l -> l.setFont(DEFAULT_FONT));
        Stream.of(new TextField[] { host, portText, user, pw }).forEach(t -> t.setFont(DEFAULT_FONT));
        Stream.of(new Control[] { usernameLabel, user, passwordLabel, pw, btnLogin })
                .forEach(c -> c.setPadding(DEFAULT_INSETS));

        loginBtn(btnLogin, host, portText, user, pw);
    }

    /**
     * 初始化聊天主界面
     * 
     * @throws Exception
     */
    private void initMain() throws Exception {
        BorderPane root = new BorderPane();
        root.setPadding(DEFAULT_INSETS);
        Scene scene = new Scene(root, HEIGHT, WIDTH);
        stageMain.setScene(scene);
        stageMain.setTitle("聊天室 by DWY");
        stageMain.getIcons().add(icon);

        root.setLeft(left);
        left.setPrefSize(200, scene.getHeight());
        left.setPadding(DEFAULT_INSETS);

        initLeft();

        root.setCenter(center);
        center.setPadding(DEFAULT_INSETS);

        initCenter();
    }

    /**
     * 初始化左方在线用户列表区域的结构
     * 
     * @throws Exception
     */
    private void initLeft() throws Exception {
        // 左主盒
        leftBox.setPadding(DEFAULT_INSETS);
        leftBox.setBorder(DEFAULT_BORDER);
        leftBox.setBackground(getColorBg(Color.TAN));
        leftBox.setPrefSize(left.getPrefWidth() - DEFAULT_PADDING * 2, left.getPrefHeight() - DEFAULT_PADDING * 2);
        left.getChildren().add(leftBox);

        // 列表标题
        Label listTitle = new Label("在线用户");
        listTitle.setPrefWidth(leftBox.getPrefWidth() - DEFAULT_PADDING * 2);
        listTitle.setPadding(DEFAULT_INSETS);
        listTitle.setAlignment(Pos.CENTER);
        listTitle.setFont(Font.font("宋体", FontWeight.BOLD, 24));
        listTitle.setBackground(getColorBg(Color.LIGHTPINK));
        leftBox.getChildren().add(listTitle);
    }

    /**
     * 初始化中心聊天区域的结构
     * 
     * @throws Exception
     */
    private void initCenter() throws Exception {
        // 消息区
        VBox centerBox = new VBox();
        centerBox.setPrefHeight(550);
        centerBox.setBorder(DEFAULT_BORDER);

        sp.setHbarPolicy(ScrollBarPolicy.NEVER);
        sp.setVbarPolicy(ScrollBarPolicy.AS_NEEDED);

        msgArea.setPadding(DEFAULT_INSETS);
        msgArea.setPrefSize(WIDTH - DEFAULT_PADDING * 6, centerBox.getPrefHeight() - DEFAULT_PADDING);
        msgArea.setMaxHeight(Double.MAX_VALUE);
        msgArea.setBackground(getColorBg(Color.LAVENDER));

        sp.setContent(msgArea);
        centerBox.getChildren().add(sp);
        center.getChildren().add(centerBox);

        // 输入区
        center.getChildren().add(getMargin(DEFAULT_PADDING * 2));
        TextArea input = new TextArea();
        input.setPrefHeight(150);
        input.setPadding(DEFAULT_INSETS);
        input.setBorder(DEFAULT_BORDER);
        input.setBackground(getColorBg(Color.LIGHTCYAN));
        center.getChildren().add(input);

        // 按钮
        center.getChildren().add(getMargin(DEFAULT_PADDING * 2));
        Button btn = getDefaultSizeBtn("发送");
        btn.setFont(DEFAULT_FONT);
        btn.setBorder(getRadiiBorder(null));
        VBox btnBox = new VBox(btn);
        btnBox.setAlignment(Pos.CENTER_RIGHT);
        center.getChildren().add(btnBox);

        sendBtn(btn, input);
    }

    /**
     * 方便设置背景色
     * 
     * @param color 要设置的颜色
     * @return 背景对象
     */
    private Background getColorBg(Color color) {
        return new Background(new BackgroundFill(color, DEFAULT_RADII, null));
    }

    /**
     * 方便设置间距
     * 
     * @param size 正方形边长
     * @return 透明盒子组件
     */
    private VBox getMargin(int size) {
        VBox margin = new VBox();
        margin.setPrefSize(size, size);
        return margin;
    }

    /**
     * 得到默认的圆角边框
     * 
     * @param color 边框颜色
     * @return 边框对象
     */
    private Border getRadiiBorder(Color color) {
        return new Border(new BorderStroke(color, null, DEFAULT_RADII, null));
    }

    /**
     * 得到默认大小的按钮
     * 
     * @param titile 按钮文字
     * @return 按钮组件
     */
    private Button getDefaultSizeBtn(String titile) {
        Button btn = new Button(titile);
        btn.setPrefSize(100, 40);
        return btn;
    }

}